﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Media;
using System.Threading.Tasks;
using DaveSexton.Labs.Properties;

namespace DaveSexton.Labs
{
	public class ConsoleLabController : LabController
	{
		#region Public Properties
		#endregion

		#region Private / Protected
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="ConsoleLabController" /> class.
		/// </summary>
		public ConsoleLabController()
		{
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="ConsoleLabController" /> class.
		/// </summary>
		public ConsoleLabController(LabCatalog catalog)
			: base(catalog)
		{
			Contract.Requires(catalog != null);
		}
		#endregion

		#region Methods
		protected override void ZeroLabs()
		{
			WriteLine(string.Empty);
			WriteLine(Resources.ZeroLabs);

			PressAnyKeyToEndProgram();
		}

		protected override void EndDebug(ICollection<Lab> labs)
		{
			PressAnyKeyToEndProgram();
		}

		protected override async void MainMenu(ICollection<Lab> labs)
		{
			do
			{
				WriteSectionTitle(Resources.MainMenu);
				WriteOption(Resources.MainMenuExecuteIndividualLabs);
				WriteOption(Resources.MainMenuExecuteAllLabs);

				ConsoleKeyInfo choice = WaitForSelection(Resources.ChoicePrompt, ConsoleKey.D1, ConsoleKey.D2);

				switch (choice.Key)
				{
					case ConsoleKey.NumPad1:
					case ConsoleKey.D1:
						await ChoiceMenuAsync(labs);
						break;
					case ConsoleKey.NumPad2:
					case ConsoleKey.D2:
						Contract.Assume(labs.Count > 0);

						var selection = WaitForSelection(Resources.ShowSourceCode, ConsoleKey.Y, ConsoleKey.N);

						if (selection.Key != ConsoleKey.Escape)
						{
							await ExecuteAsync(labs, showDescription: true, showSourceCode: selection.Key == ConsoleKey.Y);
						}
						break;
					default:  // Esc
						DetachGlobalListener();
						return;
				}

				WriteLine();
			}
			while (true);
		}

		private async Task ChoiceMenuAsync(ICollection<Lab> labs)
		{
			Contract.Requires(labs != null);
			////Contract.Requires(Contract.ForAll(labs, lab => lab != null));
			////Contract.Requires(Contract.ForAll(labs, lab => lab.IsEnabled));

			var category = LabCategory.Organize(labs);

			var parents = new Stack<LabCategory>();

			ConsoleKeyInfo choice;

			do
			{
				var hasParent = parents.Count > 0;

				var choices = WriteChoices(category, hasParent);

				choice = WaitForSelection(Resources.ChoicePrompt, choices.ToArray());
			}
			while ((category = await ExecuteChoiceAsync(choice, category, parents)) != null);
		}

		private async Task<LabCategory> ExecuteChoiceAsync(ConsoleKeyInfo choice, LabCategory category, Stack<LabCategory> parents)
		{
			Contract.Requires(category != null);
			Contract.Requires(parents != null);
			Contract.Ensures(Contract.ValueAtReturn(out category) != null);

			var c = choice.KeyChar;

			if (char.IsDigit(c))
			{
				if (c == '0')
				{
					Contract.Assume(parents.Count > 0);

					category = parents.Pop();

					Contract.Assume(category != null);
				}
				else
				{
					parents.Push(category);

					var position = (int)char.GetNumericValue(c);

					Contract.Assume(position > 0 && position < category.Subcategories.Count);

					category = category.Subcategories[position - 1];

					Contract.Assume(category != null);
				}
			}
			else
			{
				if (!await TryExecuteLabAsync(choice, category))
				{
					category = null;
				}
			}

			return category;
		}

		private async Task<bool> TryExecuteLabAsync(ConsoleKeyInfo choice, LabCategory category)
		{
			Contract.Requires(category != null);

			var c = choice.KeyChar;

			// When the Alt key is held down, KeyChar returns Alt instead of the letter; check for both.
			if (!char.IsLetter(c) && !char.TryParse(choice.Key.ToString(), out c))
			{
				// Esc
				return false;
			}

			var letter = char.ToUpperInvariant(c);

			int index = letter - 'A';

			Contract.Assume(index >= 0 && index < category.Labs.Count);

			Lab lab = category.Labs[index];

			Contract.Assume(lab != null);

			if (choice.Modifiers.HasFlag(ConsoleModifiers.Alt))
			{
				WriteLine();
				TraceSourceCode(lab.SourceCode);
			}
			else if (choice.Modifiers.HasFlag(ConsoleModifiers.Shift))
			{
				string file = System.IO.Path.GetTempFileName();

				System.IO.File.WriteAllText(file, lab.SourceCode);

				using (var process = Process.Start("notepad", "\"" + file + "\""))
				{
					Contract.Assume(process != null);

					process.WaitForInputIdle();
				}
			}
			else if (await ExecuteAsync(lab, 1, 1, showDescription: false, showSourceCode: false))
			{
				WaitForContinue();
			}

			return true;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "DaveSexton.Labs.ConsoleLabController.WriteLine(System.String,System.Nullable<System.ConsoleColor>)",
			Justification = "Text is not localizable.")]
		protected override void TraceSourceCode(string sourceCode)
		{
			if (!string.IsNullOrEmpty(sourceCode))
			{
				WriteLine(string.Empty);
				WriteLine(sourceCode.Replace("\t", "  "), ConsoleColor.Green);
			}
		}

		private ICollection<ConsoleKey> WriteChoices(LabCategory category, bool hasParent)
		{
			Contract.Requires(category != null);
			Contract.Ensures(Contract.Result<ICollection<ConsoleKey>>() != null);

			var choices = new List<ConsoleKey>(category.Subcategories.Count + category.Labs.Count);

			WriteSectionTitle(Resources.CategoryMenu);

			if (hasParent)
			{
				choices.AddRange(GetConsoleKeys('0'));

				WriteOption(Resources.ParentCategoryChoice);
			}

			if (category.Subcategories.Count > 0)
			{
				int number = 1;
				foreach (var subcategory in category.Subcategories)
				{
					Contract.Assume(subcategory != null);

					string value = number.ToString(CultureInfo.InvariantCulture);

					Contract.Assume(value.Length == 1);

					choices.AddRange(GetConsoleKeys(value[0]));

					WriteOption(Resources.ChoiceFormat, number++, subcategory.Name);
				}
			}

			if (category.Labs.Count > 0)
			{
				WriteSectionTitle(Resources.LabMenuFormat, category.Name);

				char letter = 'a';
				foreach (var lab in category.Labs)
				{
					choices.AddRange(GetConsoleKeys(letter));

					Contract.Assume(lab != null);

					WriteOption(Resources.ChoiceFormat, letter++, lab.DisplayName);

					TraceLabDescription(lab.Description);
				}

				WriteLine(string.Empty);
				WriteHint(Resources.PressShiftToOpenSourceCode);
				WriteHint(Resources.PressAltToShowSourceCode);
			}

			return choices;
		}

		private IEnumerable<ConsoleKey> GetConsoleKeys(char value)
		{
			const int a = (int)ConsoleKey.A;
			const int d0 = (int)ConsoleKey.D0;
			const int n0 = (int)ConsoleKey.NumPad0;

			if (value >= '0' && value <= '9')
			{
				yield return (ConsoleKey)(d0 + (value - '0'));
				yield return (ConsoleKey)(n0 + (value - '0'));
			}
			else if (value >= 'a' && value <= 'z')
			{
				yield return (ConsoleKey)(a + (value - 'a'));
			}
			else if (value >= 'A' && value <= 'Z')
			{
				yield return (ConsoleKey)(a + (value - 'A'));
			}
		}

		private void WriteLabHeader(string name, int number, int total, ConsoleColor color)
		{
			Contract.Requires(number > 0);
			Contract.Requires(total > 0);

			Write(Format(Resources.LabInfoFormat, name, number, total), color);
		}

		private void WriteSectionTitle(string title)
		{
			WriteLine(string.Empty);
			WriteLine(title, ConsoleColor.DarkGray);
		}

		private void WriteSectionTitle(string titleFormat, params object[] args)
		{
			WriteLine(string.Empty);
			WriteLine(Format(titleFormat, args), ConsoleColor.DarkGray);
		}

		private void WriteOption(string option)
		{
			WriteLine(option, ConsoleColor.Cyan);
		}

		private void WriteOption(string optionFormat, params object[] args)
		{
			WriteLine(Format(optionFormat, args), ConsoleColor.Cyan);
		}

		private void WriteHint(string hint)
		{
			WriteLine(hint, ConsoleColor.DarkGreen);
		}

		private void WaitForContinue()
		{
			FlushKeyboardBuffer();

			WriteLine(Resources.PressAnyKeyToContinue_Inline);

			Console.ReadKey(intercept: true);
		}

		private void PressAnyKeyToEndProgram()
		{
			if (System.Diagnostics.Debugger.IsAttached)
			{
				FlushKeyboardBuffer();

				WriteLine(Resources.PressAnyKeyToEndProgram);

				Console.ReadKey(intercept: true);
			}
		}

		internal static void FlushKeyboardBuffer()
		{
			while (Console.KeyAvailable)
			{
				Console.ReadKey(intercept: true);
			}
		}

		private ConsoleKeyInfo WaitForSelection(string prompt, params ConsoleKey[] validChoices)
		{
			Contract.Requires(prompt != null);
			Contract.Requires(validChoices != null);
			Contract.Ensures(Contract.Result<ConsoleKeyInfo>().Key == ConsoleKey.Escape
				|| validChoices.Contains(Contract.Result<ConsoleKeyInfo>().Key));

			WriteLine(string.Empty);
			Write(prompt);

			ConsoleKeyInfo choice;

			while (true)
			{
				choice = Console.ReadKey(intercept: true);

				if (validChoices.Contains(choice.Key)
					|| choice.Key == ConsoleKey.Escape)
					break;

				SystemSounds.Exclamation.Play();
			}

			var c = choice.KeyChar;

			if (char.IsLetterOrDigit(c)
				|| char.IsSymbol(c)
				|| char.IsPunctuation(c))
			{
				WriteLine(c.ToString(CultureInfo.InvariantCulture));
			}

			return choice;
		}

		protected override void TraceLabDescriptionIf(bool trace, string description)
		{
			WriteLine();

			if (trace)
			{
				WriteLine();

				TraceLabDescription(description);
			}
		}

		private void TraceLabDescription(string description)
		{
			if (!string.IsNullOrWhiteSpace(description))
			{
				description = description.Trim();

				Contract.Assume(!string.IsNullOrWhiteSpace(description));

				Write(FormatLabDescription(description), ConsoleColor.White);
			}
		}

		private static string FormatLabDescription(string description)
		{
			Contract.Requires(!string.IsNullOrWhiteSpace(description));
			Contract.Ensures(Contract.Result<string>() != null);

			return ConsoleFormat.Wrap(Resources.LabDescriptionPrefix, description);
		}

		protected override void LabStarting(Lab lab, int number, int total)
		{
			WriteLine();

			WriteLabHeader(lab.DisplayName, number, total, ConsoleColor.Cyan);
		}

		protected override async Task ExecuteAsync(Lab lab)
		{
			WriteLine();

			await base.ExecuteAsync(lab);

			WriteLine();
		}

		protected override void LabCompleted(Lab lab, int number, int total, bool executed)
		{
			WriteLabHeader(lab.DisplayName, number, total, ConsoleColor.DarkCyan);

			if (executed)
				Write(Resources.Completed_Inline, ConsoleColor.DarkCyan);

			if (number != total)
			{
				WaitForContinue();
			}
		}

		public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
		{
			WriteLine(message, eventType);
		}

		protected virtual ConsoleColor? GetTraceColor(TraceEventType eventType)
		{
			switch (eventType)
			{
				case TraceEventType.Critical:
				case TraceEventType.Error:
					return ConsoleColor.Red;
				case TraceEventType.Warning:
					return ConsoleColor.Yellow;
				case TraceEventType.Start:
				case TraceEventType.Resume:
					return ConsoleColor.Green;
				case TraceEventType.Stop:
				case TraceEventType.Suspend:
					return ConsoleColor.Magenta;
				case TraceEventType.Transfer:
					return ConsoleColor.DarkCyan;
				case TraceEventType.Verbose:
					return ConsoleColor.DarkGray;
				default:
					return null;
			}
		}

		public sealed override void Write(string message, TraceEventType eventType)
		{
			Write(message, GetTraceColor(eventType));
		}

		public sealed override void WriteLine(string message, TraceEventType eventType)
		{
			WriteLine(message, GetTraceColor(eventType));
		}

		protected virtual void Write(string message, ConsoleColor? color)
		{
			var previousColor = Console.ForegroundColor;

			if (color.HasValue)
			{
				Console.ForegroundColor = color.Value;
				Console.Write(message);
			}
			else
			{
				Console.ResetColor();
				Console.Write(message);
			}

			Console.ForegroundColor = previousColor;
		}

		protected virtual void WriteLine(string message, ConsoleColor? color)
		{
			var previousColor = Console.ForegroundColor;

			if (color.HasValue)
			{
				Console.ForegroundColor = color.Value;
				Console.WriteLine(message);
			}
			else
			{
				Console.ResetColor();
				Console.WriteLine(message);
			}

			Console.ForegroundColor = previousColor;
		}
		#endregion
	}
}